Skip to content

Conversation

@tzolov
Copy link
Contributor

@tzolov tzolov commented Oct 19, 2025

Fixes critical timing issues where MCP clients were created before all annotated beans were scanned, and tool callbacks were resolved too early during ChatClient configuration.

Problems Fixed

  1. MCP Client Timing - Clients created before all singleton beans initialized, missing late-initializing beans with MCP annotations
  2. Eager Tool Resolution - Tool callback providers resolved during configuration instead of execution
  3. Static Resolver Pollution - MCP providers incorrectly included in static resolver instead of lazy resolution
  4. List Reference Semantics - Inconsistent handling prevented reference sharing needed for deferred initialization

Solution

  • SmartInitializingSingleton Pattern: New McpSyncClientInitializer and McpAsyncClientInitializer classes defer client creation until after all singletons are instantiated
  • Lazy Resolution: ChatClient now stores providers and resolves them at execution time (call()/stream())
  • MCP Provider Filtering: Excluded from StaticToolCallbackResolver to enable lazy resolution
  • Reference Sharing: Added mcpClientsReference() methods to properly share list references

Key Changes

Configuration:

  • McpClientAutoConfiguration.java - Added initializer classes
  • McpToolCallbackAutoConfiguration.java - Uses reference sharing
  • McpClientSpecificationFactoryAutoConfiguration.java - Deprecated (disabled by default)

Tool Resolution:

  • DefaultChatClient.java - Lazy provider resolution
  • ToolCallingAutoConfiguration.java - MCP provider filtering

Providers:

  • Added mcpClientsReference() to SyncMcpToolCallbackProvider and AsyncMcpToolCallbackProvider

Breaking Changes

Deprecated (still functional):

  • mcpClients(List) and mcpClients(varargs) → Use mcpClientsReference(List) instead

Behavioral (transparent to users):

  • MCP clients created after all singleton beans instantiated
  • Tool callbacks resolved at execution time, not configuration time
  • MCP providers excluded from static resolver

Migration

Most users: No changes required - fixes are transparent and backward compatible.
Custom MCP providers: Update to use mcpClientsReference() instead of deprecated mcpClients() methods.

Resolves: #4670, #4618

Resolves critical timing issues in MCP client initialization and
tool callback resolution that prevented proper registration of
MCP-annotated beans.

- MCP clients created after all singleton beans initialized
  - Implement SmartInitializingSingleton for deferred client creation
    via McpSyncClientInitializer and McpAsyncClientInitializer
- Tool callbacks resolved at execution instead configuration
  - Store ToolCallbackProvider instances in ChatClient and resolve
    lazily at execution time (call/stream)
- Filter MCP providers from StaticToolCallbackResolver
- Inconsistent list reference handling
  - Add mcpClientsReference() methods for proper reference sharing

Breaking Changes:
- MCP providers no longer in static resolver (transparent)
- Client creation timing changed (transparent)

Fixes: spring-projects#4670, spring-projects#4618

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
@tzolov tzolov force-pushed the lazy-mcp-client-and-toolcallback-resolution-2 branch from 98ad131 to 2b8bd13 Compare October 19, 2025 13:01
@tzolov tzolov added this to the 1.1.0.M4 milestone Oct 19, 2025
@tzolov tzolov marked this pull request as ready for review October 19, 2025 13:12
.toolContextToMcpMetaConverter(
toolContextToMcpMetaConverter.getIfUnique(() -> ToolContextToMcpMetaConverter.defaultConverter()))
.mcpClients(mcpClients)
.mcpClientsReference(mcpClients)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it can use clients instead of mcpClients. after all. the mcpClients is deprecated and mcpClientsReference is too long

Copy link
Contributor

@kuntal1461 kuntal1461 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ilayaperumalg The lazy resolution improvements look solid overall.
• The mutate() path needs to retain any pending ToolCallbackProvider instances so they persist after mutation.
• The annotation-scanner condition change currently reverses user intent when disabling the scanner; enabled=false should keep it off rather than re-enable it.

@Deprecated(since = "1.1.0", forRemoval = true)
@AutoConfiguration(after = McpClientAnnotationScannerAutoConfiguration.class)
@ConditionalOnClass(McpLogging.class)
@ConditionalOnProperty(prefix = McpClientAnnotationScannerProperties.CONFIG_PREFIX, name = "enabled",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @tzolov – flipping the condition to havingValue="false" unintentionally re-enables this (deprecated) auto-configuration whenever a user sets spring.ai.mcp.client.annotation-scanner.enabled=false. In 1.1.0‑M2 that property meant “turn it off”; with this change the same setting now activates it, so existing apps that disabled the scanner will regress. If the goal is to keep it off by default, the condition should probably remain tied to true (with a new default of false) or be removed entirely.

* settings are replicated from this {@link ChatClientRequest}.
*/
@Override
public Builder mutate() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @tzolov – DefaultChatClientRequestSpec.mutate() only copies the already-resolved toolCallbacks list into the new builder and drops the still‑pending toolCallbackProviders. If a request spec is configured with toolCallbacks(provider) and then mutated before executing, the new client silently loses the provider and no MCP tools are ever resolved. (Repro: call chatClient.prompt().toolCallbacks(provider).mutate().build().prompt().call() – the provider is never invoked.) Please carry the providers over as well, e.g. add defaultToolCallbacks(this.toolCallbackProviders.toArray(new ToolCallbackProvider[0])) before returning the builder, or otherwise ensure they survive mutation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP Client Initialization Timing and Tool Callback Resolution Issues

4 participants